package jxtend.processor.eclipse.visitor.handler.impl;

import jxtend.processor.eclipse.utils.ASTNodeKind;
import jxtend.processor.eclipse.utils.ASTNodeUtils;
import jxtend.processor.eclipse.utils.EclipseProcessorUtils;
import jxtend.processor.eclipse.visitor.handler.ASTNodeHandlerVisitorContext;
import jxtend.processor.eclipse.visitor.handler.AbstractASTNodeVisitorHandler;
import jxtend.processor.eclipse.visitor.handler.ProblemSeverity;

import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.codeassist.CompletionEngine;
import org.eclipse.jdt.internal.codeassist.complete.CompletionOnSingleTypeReference;
import org.eclipse.jdt.internal.codeassist.complete.CompletionParser;
import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.AllocationExpression;
import org.eclipse.jdt.internal.compiler.ast.Annotation;
import org.eclipse.jdt.internal.compiler.ast.Argument;
import org.eclipse.jdt.internal.compiler.ast.Expression;
import org.eclipse.jdt.internal.compiler.ast.FieldDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Initializer;
import org.eclipse.jdt.internal.compiler.ast.MarkerAnnotation;
import org.eclipse.jdt.internal.compiler.ast.MethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.ParameterizedQualifiedTypeReference;
import org.eclipse.jdt.internal.compiler.ast.ParameterizedSingleTypeReference;
import org.eclipse.jdt.internal.compiler.ast.QualifiedAllocationExpression;
import org.eclipse.jdt.internal.compiler.ast.SingleNameReference;
import org.eclipse.jdt.internal.compiler.ast.SingleTypeReference;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.ast.TypeReference;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.eclipse.jdt.internal.compiler.lookup.BlockScope;
import org.eclipse.jdt.internal.compiler.lookup.ExtraCompilerModifiers;
import org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.Scope;
import org.eclipse.jdt.internal.compiler.lookup.TypeConstants;

@SuppressWarnings("restriction")
public class SAMTypeInstantiationShorthandHandler
		extends AbstractASTNodeVisitorHandler { //implements StatementTypeResolver {

	@Override
	public void beginVisit(ASTNodeHandlerVisitorContext context, ASTNode node, Scope scope) {
		ASTNode parentNode = context.getParentNode();
		if (node instanceof ParameterizedSingleTypeReference
				&& CharOperation.equals(JxtendTypeConstants.SAM_TOKEN, ((ParameterizedSingleTypeReference) node).token)
				&& ((ParameterizedSingleTypeReference) node).typeArguments.length == 1
				&& parentNode instanceof AllocationExpression) {
			TypeReference completionOnTypeReference = ((ParameterizedSingleTypeReference) node).typeArguments[0];

			TypeReference[][] typeArguments = new TypeReference[JxtendTypeConstants.SAM_TOKEN_CLASS.length][];
			typeArguments[JxtendTypeConstants.SAM_TOKEN_CLASS.length - 1] =
					new TypeReference[] { completionOnTypeReference };

			ParameterizedQualifiedTypeReference newQualifiedAllocationExpressionType =
					ASTNodeUtils.createParameterizedQualifiedTypeReference(
							ASTNodeKind.getEnum(node), JxtendTypeConstants.SAM_TOKEN_CLASS,
							typeArguments, node.sourceStart, node.sourceEnd);
			newQualifiedAllocationExpressionType.resolvedType =
					scope.getType(newQualifiedAllocationExpressionType.tokens,
							newQualifiedAllocationExpressionType.tokens.length);

//			newQualifiedAllocationExpressionType.bits |= ASTNodeBits.JXTEND_GENERATED;
//
//			newQualifiedAllocationExpressionType.resolve((BlockScope) scope);
			
			context.replaceNode(newQualifiedAllocationExpressionType);
		} else if (node instanceof CompletionOnSingleTypeReference
				&& parentNode instanceof ParameterizedQualifiedTypeReference
				&& CharOperation.equals(
						JxtendTypeConstants.SAM_TOKEN_CLASS, ((ParameterizedQualifiedTypeReference) parentNode).tokens)
				&& ((ParameterizedQualifiedTypeReference) parentNode)
						.typeArguments[JxtendTypeConstants.SAM_TOKEN_CLASS.length - 1].length == 1
				&& context.getParentNode(1) instanceof AllocationExpression) {

			if (context.getLookupEnvironment().typeRequestor instanceof CompletionEngine) {
				((CompletionParser) ((CompletionEngine) context.getLookupEnvironment().typeRequestor).getParser())
					.assistNodeParent = parentNode;
			}
		}
	}

	@Override
	public boolean endVisitBeforeResolve(ASTNodeHandlerVisitorContext context, ASTNode node, Scope scope) {
		ASTNode parentNode = context.getParentNode();

		if (!(scope instanceof BlockScope)
				|| !(node instanceof TypeReference)
				|| !isValidTypeReference((TypeReference) node)) {
			return true;
		}

		TypeReference typeArgument = ((ParameterizedQualifiedTypeReference) node).typeArguments[
		        ((ParameterizedQualifiedTypeReference) node).typeArguments.length - 1][0];

		if ((typeArgument instanceof CompletionOnSingleTypeReference)
				&& (!(parentNode instanceof QualifiedAllocationExpression)
					|| (node != ((QualifiedAllocationExpression) parentNode).type)
					|| (((QualifiedAllocationExpression) parentNode).anonymousType == null))) {
			return true;
		}

//		context.addImport(JxtendTypeConstants.SAM_TOKEN_CLASS);

//		if (typeArgument instanceof CompletionOnSingleTypeReference) {
////			long samKeywordPos = EclipseProcessorUtils.encodePos(qualifiedAllocationExpression.type.sourceStart + 1,
////					qualifiedAllocationExpression.type.sourceStart + JxtendTypeConstants.SAM_TOKEN.length);
//
//			TypeReference[][] typeArguments = new TypeReference[JxtendTypeConstants.SAM_TOKEN_CLASS.length][];
//			typeArguments[JxtendTypeConstants.SAM_TOKEN_CLASS.length - 1] = new TypeReference[] { typeArgument };
//			ASTNode newQualifiedAllocationExpressionType = ASTNodeUtils.createParameterizedQualifiedTypeReference(
//					ASTNodeKind.getEnum(node), JxtendTypeConstants.SAM_TOKEN_CLASS,
//					typeArguments, node.sourceStart, node.sourceEnd);
//			newQualifiedAllocationExpressionType.bits |= ASTNodeBits.JXTEND_GENERATED;
//
//			context.replaceNode(newQualifiedAllocationExpressionType);
//
//			return true;
//		}

		QualifiedAllocationExpression qualifiedAllocationExpression = (QualifiedAllocationExpression) parentNode;
		TypeDeclaration anonymousType = qualifiedAllocationExpression.anonymousType;
		
//		if (!(node instanceof))
		if (ASTNodeKind.getEnum(qualifiedAllocationExpression) == ASTNodeKind.NORMAL
				&& !validAnonymousType(context, qualifiedAllocationExpression)) {
			return false;
		}

		MethodBinding singleAbstractMethodBinding =
			singleAbstractMethodBinding(context, (BlockScope) scope, qualifiedAllocationExpression);
		if (singleAbstractMethodBinding == null) {
			return false;
		}

		TypeReference targetType = qualifiedAllocationExpression.type;
		ASTNodeKind astNodeKind = ASTNodeKind.getEnum(targetType);
	
		if (ASTNodeKind.getEnum(qualifiedAllocationExpression) == ASTNodeKind.NORMAL) {

		Initializer sourceInitializer = (Initializer) qualifiedAllocationExpression.anonymousType.fields[0];

		long pos = EclipseProcessorUtils.encodePos(sourceInitializer.sourceStart, sourceInitializer.sourceStart + 1);

		MethodDeclaration anonymousTypeMethod = 
			new MethodDeclaration(context.findParentNode(AbstractMethodDeclaration.class).compilationResult);
		anonymousTypeMethod.modifiers = ClassFileConstants.AccPublic;
		anonymousTypeMethod.returnType = TypeUtils.createTypeReference(singleAbstractMethodBinding.returnType, pos);
		anonymousTypeMethod.selector =  singleAbstractMethodBinding.selector;

		anonymousTypeMethod.sourceStart = sourceInitializer.sourceStart;
		anonymousTypeMethod.modifiersSourceStart = sourceInitializer.modifiersSourceStart;
		anonymousTypeMethod.declarationSourceStart = sourceInitializer.declarationSourceStart;
		anonymousTypeMethod.declarationSourceEnd = sourceInitializer.declarationSourceEnd;
		anonymousTypeMethod.bodyStart = sourceInitializer.bodyStart;
		anonymousTypeMethod.bodyEnd = sourceInitializer.bodyEnd;
		anonymousTypeMethod.sourceEnd = sourceInitializer.sourceEnd;

		anonymousTypeMethod.statements = sourceInitializer.block.statements;

		MarkerAnnotation overrideAnnotation =
			new MarkerAnnotation(new SingleTypeReference("Override".toCharArray(), pos), targetType.sourceStart);
		overrideAnnotation.sourceEnd = targetType.sourceEnd;
		anonymousTypeMethod.annotations = new Annotation[1];
		anonymousTypeMethod.annotations[0] = overrideAnnotation;

		anonymousTypeMethod.arguments =
			createSingleAbstractMethodParameters(context, qualifiedAllocationExpression, singleAbstractMethodBinding);

		qualifiedAllocationExpression.arguments = null;

		anonymousType.fields = null;

		anonymousType.methods = new AbstractMethodDeclaration[1];
		anonymousType.methods[0] = anonymousTypeMethod;

		}
		
		if (Thread.currentThread().getName().equals("org.eclipse.jdt.internal.ui.text.JavaReconciler")) {
			FieldDeclaration unusedFieldWithOriginalType =
				new FieldDeclaration(JxtendTypeConstants.SAM_TOKEN, node.sourceStart + 1, node.sourceEnd);
			unusedFieldWithOriginalType.modifiers = ClassFileConstants.AccPrivate;

			TypeReference[][] typeArguments = //new TypeReference[JxtendTypeConstants.SAM_TOKEN_CLASS.length][];
//			typeArguments[JxtendTypeConstants.SAM_TOKEN_CLASS.length - 1] = 
					((ParameterizedQualifiedTypeReference) node).typeArguments;
			unusedFieldWithOriginalType.type = ASTNodeUtils.createParameterizedQualifiedTypeReference(
					ASTNodeKind.getEnum(node), JxtendTypeConstants.SAM_TOKEN_CLASS, typeArguments,
					node.sourceStart, node.sourceEnd);

			context.resolveExpression(unusedFieldWithOriginalType.type, (BlockScope) scope);

			context.appendField(qualifiedAllocationExpression.anonymousType, unusedFieldWithOriginalType);
			unusedFieldWithOriginalType.modifiers |= ExtraCompilerModifiers.AccLocallyUsed;
		}


		char[][] compoundName = singleAbstractMethodBinding.declaringClass.compoundName;

		long samKeywordPos = EclipseProcessorUtils.encodePos(qualifiedAllocationExpression.type.sourceStart + 1,
				qualifiedAllocationExpression.type.sourceStart + JxtendTypeConstants.SAM_TOKEN.length);

		ASTNode newQualifiedAllocationExpressionType = ASTNodeUtils.createQualifiedTypeReference(
				astNodeKind, compoundName, ASTNodeUtils.encodePosArray(compoundName.length, samKeywordPos));
		newQualifiedAllocationExpressionType.bits |= ASTNodeBits.JXTEND_GENERATED;

		context.replaceNode(newQualifiedAllocationExpressionType);

		return true;
	}

	private static boolean isValidTypeReference(TypeReference typeReference) {
		boolean valid = (typeReference instanceof ParameterizedQualifiedTypeReference)
			&& CharOperation.equals(
					JxtendTypeConstants.SAM_TOKEN_CLASS, ((ParameterizedQualifiedTypeReference) typeReference).tokens)
			&& ((ParameterizedQualifiedTypeReference) typeReference).typeArguments != null
			&& ((ParameterizedQualifiedTypeReference) typeReference).typeArguments[
			        ((ParameterizedQualifiedTypeReference) typeReference).typeArguments.length - 1].length == 1;

		if (!valid) {
			return false;
		}

		TypeReference typeArgument = ((ParameterizedQualifiedTypeReference) typeReference).typeArguments[
		        ((ParameterizedQualifiedTypeReference) typeReference).typeArguments.length - 1][0];

		return (typeArgument instanceof CompletionOnSingleTypeReference)
			|| ((typeArgument.resolvedType != null)
					&& EclipseProcessorUtils.isValidTypeReference(typeArgument.resolvedType));
	}

	private static Argument[] createSingleAbstractMethodParameters(ASTNodeHandlerVisitorContext context,
			QualifiedAllocationExpression qualifiedAllocationExpression, MethodBinding singleAbstractMethodBinding) {
		if (qualifiedAllocationExpression.arguments == null) {
			return new Argument[0];
		}

		if ((singleAbstractMethodBinding.parameters != null)
				&& (singleAbstractMethodBinding.parameters.length != qualifiedAllocationExpression.arguments.length)) {
			context.recordProblemForNode(ProblemSeverity.ERROR, context.getCurrentReferenceContext(),
					qualifiedAllocationExpression.sourceStart, qualifiedAllocationExpression.type.statementEnd,
					"Number or contructor arguments does not match argument count of single abstract method");

			return new Argument[0];
		}
		
		Argument[] result = new Argument[qualifiedAllocationExpression.arguments.length];

		boolean invalidArguments = false;
		
		Expression[] constructorArguments = qualifiedAllocationExpression.arguments;
		for (int i = 0; i < constructorArguments.length; i++) {
			if (!(constructorArguments[i] instanceof SingleNameReference)) {
				context.recordProblemForNode(ProblemSeverity.ERROR, context.getCurrentReferenceContext(),
						constructorArguments[i].sourceStart, constructorArguments[i].statementEnd,
						"Argument expression must be an unqualified name reference");

				invalidArguments = true;
			}

			SingleNameReference argumentNameReference = (SingleNameReference) constructorArguments[i];

			ASTNodeKind astNodeKind = ASTNodeKind.getEnum(argumentNameReference);

			long pos =
				EclipseProcessorUtils.encodePos(argumentNameReference.sourceStart, argumentNameReference.sourceEnd);

			char[] argumentName = argumentNameReference.token;
			if (CharOperation.equals(JxtendTypeConstants.UNDERSCORE, argumentName)) {
				argumentName = CharOperation.concat(
							JxtendTypeConstants.INTERNAL_ARGUMENT_PREFIX, Integer.toString(i + 1).toCharArray());
			}
			
			result[i] = ASTNodeUtils.createArgument(astNodeKind, argumentName, pos,
					TypeUtils.createTypeReference(
							singleAbstractMethodBinding.parameters[i], pos), 0); //ClassFileConstants.AccFinal);
		}

		if (invalidArguments) {
			return new Argument[0];
		}

		return result;
	}

	private static MethodBinding singleAbstractMethodBinding(ASTNodeHandlerVisitorContext context,
			BlockScope blockScope, QualifiedAllocationExpression qualifiedAllocationExpression) {
		ReferenceBinding targetType = (ReferenceBinding)
			((ParameterizedQualifiedTypeReference) qualifiedAllocationExpression.type).typeArguments[
			    ((ParameterizedQualifiedTypeReference) qualifiedAllocationExpression.type).typeArguments.length - 1][0].resolvedType;

		if ((targetType.superclass() == null)
				|| !CharOperation.equals(TypeConstants.JAVA_LANG_OBJECT, targetType.superclass().compoundName)) {
			context.recordProblemForNode(ProblemSeverity.ERROR, context.getCurrentReferenceContext(),
					qualifiedAllocationExpression.sourceStart, qualifiedAllocationExpression.type.statementEnd,
					"SAM-type conversion currently only implemented for interface types.");
			return null;
		}

		if (targetType.superInterfaces().length != 0) {
			context.recordProblemForNode(ProblemSeverity.ERROR, context.getCurrentReferenceContext(),
					qualifiedAllocationExpression.sourceStart, qualifiedAllocationExpression.type.statementEnd,
					"SAM-type conversion currently only implemented for interface types with no super interfaces.");
			return null;
		}

		MethodBinding abstractMethodBinding = null;
		for (MethodBinding methodBinding : targetType.methods()) {
			if ((methodBinding.modifiers & ClassFileConstants.AccAbstract) != 0) {
				if (abstractMethodBinding != null) {
					context.recordProblemForNode(ProblemSeverity.ERROR, context.getCurrentReferenceContext(),
							qualifiedAllocationExpression.sourceStart, qualifiedAllocationExpression.type.statementEnd,
							"Target type is not a SAM-type since it has more than one abstract method");

					return null;
				}

				abstractMethodBinding = methodBinding;
			}
		}
		
		if (abstractMethodBinding == null) {
			context.recordProblemForNode(ProblemSeverity.ERROR, context.getCurrentReferenceContext(),
					qualifiedAllocationExpression.sourceStart, qualifiedAllocationExpression.type.statementEnd,
					"Target type is not a SAM-type since it has no abstract methods");
			return null;
		}

		return abstractMethodBinding;
	}

	private static boolean validAnonymousType(
			ASTNodeHandlerVisitorContext context, QualifiedAllocationExpression qualifiedAllocationExpression) {
		TypeDeclaration anonymousType = qualifiedAllocationExpression.anonymousType;

		if ((anonymousType.fields == null) || (anonymousType.fields.length == 0)) {
			context.recordProblemForNode(ProblemSeverity.ERROR, context.getCurrentReferenceContext(),
					qualifiedAllocationExpression.sourceStart, qualifiedAllocationExpression.type.sourceEnd,
					"SAM type can't be modified to anonymous class without an initializer expression");
			return false;
		} else {
			FieldDeclaration[] fields = anonymousType.fields;
			assert fields.length > 0;

			boolean errorReported = false;
			for (FieldDeclaration fieldDeclaration : fields) {
				if (!(fieldDeclaration instanceof Initializer)
						|| ((((Initializer) fieldDeclaration).modifiers & ClassFileConstants.AccStatic) != 0)) {
					context.recordProblemForNode(ProblemSeverity.ERROR, context.getCurrentReferenceContext(),
							fieldDeclaration.declarationSourceStart, fieldDeclaration.declarationSourceEnd,
							"Fields or static initializers are not allowed");
					errorReported = true;
				}
			}
			
			if (errorReported) {
				return false;
			}

			assert (fields[0] instanceof Initializer)
				&& ((((Initializer) fields[0]).modifiers & ClassFileConstants.AccStatic) == 0);

			if (fields.length != 1) {
				for (int i = 1; i < fields.length; i++) {
					FieldDeclaration fieldDeclaration = fields[i];
					assert (fieldDeclaration instanceof Initializer)
						&& ((((Initializer) fieldDeclaration).modifiers & ClassFileConstants.AccStatic) == 0);

					context.recordProblemForNode(ProblemSeverity.ERROR, context.getCurrentReferenceContext(),
							fieldDeclaration.declarationSourceStart, fieldDeclaration.declarationSourceEnd,
							"Only one (non-static) initializer experssion is allowed");
					errorReported = true;
				}

				if (errorReported) {
					return false;
				}
			}
		}

		if ((anonymousType.methods != null) && (anonymousType.methods.length != 0)) {
			for (AbstractMethodDeclaration abstractMethodDeclaration : anonymousType.methods) {
				context.recordProblemForNode(ProblemSeverity.ERROR, context.getCurrentReferenceContext(),
						abstractMethodDeclaration.declarationSourceStart,
						abstractMethodDeclaration.declarationSourceEnd, "No metholds are allowed");
			}

			return false;				
		}

		return true;
	}

}
